(function(global, factory) {
    typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.jinja = {}))
})(this, function(jinja) {
    "use strict";
    var STRINGS = /'(\\.|[^'])*'|"(\\.|[^"'"])*"/g;
    var IDENTS_AND_NUMS = /([$_a-z][$\w]*)|([+-]?\d+(\.\d+)?)/g;
    var NUMBER = /^[+-]?\d+(\.\d+)?$/;
    var NON_PRIMITIVES = /\[[@#~](,[@#~])*\]|\[\]|\{([@i]:[@#~])(,[@i]:[@#~])*\}|\{\}/g;
    var IDENTIFIERS = /[$_a-z][$\w]*/gi;
    var VARIABLES = /i(\.i|\[[@#i]\])*/g;
    var ACCESSOR = /(\.i|\[[@#i]\])/g;
    var OPERATORS = /(===?|!==?|>=?|<=?|&&|\|\||[+\-\*\/%])/g;
    var EOPS = /(^|[^$\w])(and|or|not|is|isnot)([^$\w]|$)/g;
    var LEADING_SPACE = /^\s+/;
    var TRAILING_SPACE = /\s+$/;
    var START_TOKEN = /\{\{\{|\{\{|\{%|\{#/;
    var TAGS = {
        "{{{": /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?\}\}\}/,
        "{{": /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?\}\}/,
        "{%": /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?%\}/,
        "{#": /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?#\}/
    };
    var delimeters = {
        "{%": "directive",
        "{{": "output",
        "{#": "comment"
    };
    var operators = {
        and: "&&",
        or: "||",
        not: "!",
        is: "==",
        isnot: "!="
    };
    var constants = {
        true: true,
        false: false,
        null: null
    };

    function Parser() {
        this.nest = [];
        this.compiled = [];
        this.childBlocks = 0;
        this.parentBlocks = 0;
        this.isSilent = false
    }
    Parser.prototype.push = function(line) {
        if (!this.isSilent) {
            this.compiled.push(line)
        }
    };
    Parser.prototype.parse = function(src) {
        this.tokenize(src);
        return this.compiled
    };
    Parser.prototype.tokenize = function(src) {
        var lastEnd = 0,
            parser = this,
            trimLeading = false;
        matchAll(src, START_TOKEN, function(open, index, src) {
            var match = src.slice(index + open.length).match(TAGS[open]);
            match = match ? match[0] : "";
            var simplified = match.replace(STRINGS, "@");
            if (!match || ~simplified.indexOf(open)) {
                return index + 1
            }
            var inner = match.slice(0, 0 - open.length);
            if (inner.charAt(0) === "-") var wsCollapseLeft = true;
            if (inner.slice(-1) === "-") var wsCollapseRight = true;
            inner = inner.replace(/^-|-$/g, "").trim();
            if (parser.rawMode && open + inner !== "{%endraw") {
                return index + 1
            }
            var text = src.slice(lastEnd, index);
            lastEnd = index + open.length + match.length;
            if (trimLeading) text = trimLeft(text);
            if (wsCollapseLeft) text = trimRight(text);
            if (wsCollapseRight) trimLeading = true;
            if (open === "{{{") {
                open = "{{";
                inner += "|safe"
            }
            parser.textHandler(text);
            parser.tokenHandler(open, inner)
        });
        var text = src.slice(lastEnd);
        if (trimLeading) text = trimLeft(text);
        this.textHandler(text)
    };
    Parser.prototype.textHandler = function(text) {
        this.push("write(" + JSON.stringify(text) + ");")
    };
    Parser.prototype.tokenHandler = function(open, inner) {
        var type = delimeters[open];
        if (type === "directive") {
            this.compileTag(inner)
        } else if (type === "output") {
            var extracted = this.extractEnt(inner, STRINGS, "@");
            extracted.src = extracted.src.replace(/\|\|/g, "~").split("|");
            extracted.src = extracted.src.map(function(part) {
                return part.split("~").join("||")
            });
            var parts = this.injectEnt(extracted, "@");
            if (parts.length > 1) {
                var filters = parts.slice(1).map(this.parseFilter.bind(this));
                this.push("filter(" + this.parseExpr(parts[0]) + "," + filters.join(",") + ");")
            } else {
                this.push("filter(" + this.parseExpr(parts[0]) + ");")
            }
        }
    };
    Parser.prototype.compileTag = function(str) {
        var directive = str.split(" ")[0];
        var handler = tagHandlers[directive];
        if (!handler) {
            throw new Error("Invalid tag: " + str)
        }
        handler.call(this, str.slice(directive.length).trim())
    };
    Parser.prototype.parseFilter = function(src) {
        src = src.trim();
        var match = src.match(/[:(]/);
        var i = match ? match.index : -1;
        if (i < 0) return JSON.stringify([src]);
        var name = src.slice(0, i);
        var args = src.charAt(i) === ":" ? src.slice(i + 1) : src.slice(i + 1, -1);
        args = this.parseExpr(args, {
            terms: true
        });
        return "[" + JSON.stringify(name) + "," + args + "]"
    };
    Parser.prototype.extractEnt = function(src, regex, placeholder) {
        var subs = [],
            isFunc = typeof placeholder == "function";
        src = src.replace(regex, function(str) {
            var replacement = isFunc ? placeholder(str) : placeholder;
            if (replacement) {
                subs.push(str);
                return replacement
            }
            return str
        });
        return {
            src: src,
            subs: subs
        }
    };
    Parser.prototype.injectEnt = function(extracted, placeholder) {
        var src = extracted.src,
            subs = extracted.subs,
            isArr = Array.isArray(src);
        var arr = isArr ? src : [src];
        var re = new RegExp("[" + placeholder + "]", "g"),
            i = 0;
        arr.forEach(function(src, index) {
            arr[index] = src.replace(re, function() {
                return subs[i++]
            })
        });
        return isArr ? arr : arr[0]
    };
    Parser.prototype.replaceComplex = function(s) {
        var parsed = this.extractEnt(s, /i(\.i|\[[@#i]\])+/g, "v");
        parsed.src = parsed.src.replace(NON_PRIMITIVES, "~");
        return this.injectEnt(parsed, "v")
    };
    Parser.prototype.parseExpr = function(src, opts) {
        opts = opts || {};
        var parsed1 = this.extractEnt(src, STRINGS, "@");
        parsed1.src = parsed1.src.replace(EOPS, function(s, before, op, after) {
            return op in operators ? before + operators[op] + after : s
        });
        var parsed2 = this.extractEnt(parsed1.src, IDENTS_AND_NUMS, function(s) {
            return s in constants || NUMBER.test(s) ? "#" : null
        });
        var parsed3 = this.extractEnt(parsed2.src, IDENTIFIERS, "i");
        parsed3.src = parsed3.src.replace(/\s+/g, "");
        var simplified = parsed3.src;
        while (simplified !== (simplified = this.replaceComplex(simplified)));
        while (simplified !== (simplified = simplified.replace(/i(\.i|\[[@#i]\])+/, "v")));
        simplified = simplified.replace(/[iv]\[v?\]/g, "x");
        simplified = simplified.replace(/[@#~v]/g, "i");
        simplified = simplified.replace(OPERATORS, "%");
        simplified = simplified.replace(/!+[i]/g, "i");
        var terms = opts.terms ? simplified.split(",") : [simplified];
        terms.forEach(function(term) {
            while (term !== (term = term.replace(/\(i(%i)*\)/g, "i")));
            if (!term.match(/^i(%i)*/)) {
                throw new Error("Invalid expression: " + src + " " + term)
            }
        });
        parsed3.src = parsed3.src.replace(VARIABLES, this.parseVar.bind(this));
        parsed2.src = this.injectEnt(parsed3, "i");
        parsed1.src = this.injectEnt(parsed2, "#");
        return this.injectEnt(parsed1, "@")
    };
    Parser.prototype.parseVar = function(src) {
        var args = Array.prototype.slice.call(arguments);
        var str = args.pop(),
            index = args.pop();
        if (src === "i" && str.charAt(index + 1) === ":") {
            return '"i"'
        }
        var parts = ['"i"'];
        src.replace(ACCESSOR, function(part) {
            if (part === ".i") {
                parts.push('"i"')
            } else if (part === "[i]") {
                parts.push('get("i")')
            } else {
                parts.push(part.slice(1, -1))
            }
        });
        return "get(" + parts.join(",") + ")"
    };
    Parser.prototype.escName = function(str) {
        return str.replace(/\W/g, function(s) {
            return "$" + s.charCodeAt(0).toString(16)
        })
    };
    Parser.prototype.parseQuoted = function(str) {
        if (str.charAt(0) === "'") {
            str = str.slice(1, -1).replace(/\\.|"/, function(s) {
                if (s === "\\'") return "'";
                return s.charAt(0) === "\\" ? s : "\\" + s
            });
            str = '"' + str + '"'
        }
        return JSON.parse(str)
    };
    var tagHandlers = {
        if: function(expr) {
            this.push("if (" + this.parseExpr(expr) + ") {");
            this.nest.unshift("if")
        },
        else: function() {
            if (this.nest[0] === "for") {
                this.push("}, function() {")
            } else {
                this.push("} else {")
            }
        },
        elseif: function(expr) {
            this.push("} else if (" + this.parseExpr(expr) + ") {")
        },
        endif: function() {
            this.nest.shift();
            this.push("}")
        },
        for: function(str) {
            var i = str.indexOf(" in ");
            var name = str.slice(0, i).trim();
            var expr = str.slice(i + 4).trim();
            this.push("each(" + this.parseExpr(expr) + "," + JSON.stringify(name) + ",function() {");
            this.nest.unshift("for")
        },
        endfor: function() {
            this.nest.shift();
            this.push("});")
        },
        raw: function() {
            this.rawMode = true
        },
        endraw: function() {
            this.rawMode = false
        },
        set: function(stmt) {
            var i = stmt.indexOf("=");
            var name = stmt.slice(0, i).trim();
            var expr = stmt.slice(i + 1).trim();
            this.push("set(" + JSON.stringify(name) + "," + this.parseExpr(expr) + ");")
        },
        block: function(name) {
            if (this.isParent) {
                ++this.parentBlocks;
                var blockName = "block_" + (this.escName(name) || this.parentBlocks);
                this.push("block(typeof " + blockName + ' == "function" ? ' + blockName + " : function() {")
            } else if (this.hasParent) {
                this.isSilent = false;
                ++this.childBlocks;
                blockName = "block_" + (this.escName(name) || this.childBlocks);
                this.push("function " + blockName + "() {")
            }
            this.nest.unshift("block")
        },
        endblock: function() {
            this.nest.shift();
            if (this.isParent) {
                this.push("});")
            } else if (this.hasParent) {
                this.push("}");
                this.isSilent = true
            }
        },
        extends: function(name) {
            name = this.parseQuoted(name);
            var parentSrc = this.readTemplateFile(name);
            this.isParent = true;
            this.tokenize(parentSrc);
            this.isParent = false;
            this.hasParent = true;
            this.isSilent = true
        },
        include: function(name) {
            name = this.parseQuoted(name);
            var incSrc = this.readTemplateFile(name);
            this.isInclude = true;
            this.tokenize(incSrc);
            this.isInclude = false
        }
    };
    tagHandlers.assign = tagHandlers.set;
    tagHandlers.elif = tagHandlers.elseif;
    var getRuntime = function runtime(data, opts) {
        var defaults = {
            autoEscape: "toJson"
        };
        var _toString = Object.prototype.toString;
        var _hasOwnProperty = Object.prototype.hasOwnProperty;
        var getKeys = Object.keys || function(obj) {
            var keys = [];
            for (var n in obj)
                if (_hasOwnProperty.call(obj, n)) keys.push(n);
            return keys
        };
        var isArray = Array.isArray || function(obj) {
            return _toString.call(obj) === "[object Array]"
        };
        var create = Object.create || function(obj) {
            function F() {}
            F.prototype = obj;
            return new F
        };
        var toString = function(val) {
            if (val == null) return "";
            return typeof val.toString == "function" ? val.toString() : _toString.call(val)
        };
        var extend = function(dest, src) {
            var keys = getKeys(src);
            for (var i = 0, len = keys.length; i < len; i++) {
                var key = keys[i];
                dest[key] = src[key]
            }
            return dest
        };
        var get = function() {
            var val, n = arguments[0],
                c = stack.length;
            while (c--) {
                val = stack[c][n];
                if (typeof val != "undefined") break
            }
            for (var i = 1, len = arguments.length; i < len; i++) {
                if (val == null) continue;
                n = arguments[i];
                val = _hasOwnProperty.call(val, n) ? val[n] : typeof val._get == "function" ? val[n] = val._get(n) : null
            }
            return val == null ? "" : val
        };
        var set = function(n, val) {
            stack[stack.length - 1][n] = val
        };
        var push = function(ctx) {
            stack.push(ctx || {})
        };
        var pop = function() {
            stack.pop()
        };
        var write = function(str) {
            output.push(str)
        };
        var filter = function(val) {
            for (var i = 1, len = arguments.length; i < len; i++) {
                var arr = arguments[i],
                    name = arr[0],
                    filter = filters[name];
                if (filter) {
                    arr[0] = val;
                    val = filter.apply(data, arr)
                } else {
                    throw new Error("Invalid filter: " + name)
                }
            }
            if (opts.autoEscape && name !== opts.autoEscape && name !== "safe") {
                val = filters[opts.autoEscape].call(data, val)
            }
            output.push(val)
        };
        var each = function(obj, loopvar, fn1, fn2) {
            if (obj == null) return;
            var arr = isArray(obj) ? obj : getKeys(obj),
                len = arr.length;
            var ctx = {
                loop: {
                    length: len,
                    first: arr[0],
                    last: arr[len - 1]
                }
            };
            push(ctx);
            for (var i = 0; i < len; i++) {
                extend(ctx.loop, {
                    index: i + 1,
                    index0: i
                });
                fn1(ctx[loopvar] = arr[i])
            }
            if (len === 0 && fn2) fn2();
            pop()
        };
        var block = function(fn) {
            push();
            fn();
            pop()
        };
        var render = function() {
            return output.join("")
        };
        data = data || {};
        opts = extend(defaults, opts || {});
        var filters = extend({
            html: function(val) {
                return toString(val).split("&").join("&amp;").split("<").join("&lt;").split(">").join("&gt;").split('"').join("&quot;")
            },
            safe: function(val) {
                return val
            },
            toJson: function(val) {
                if (typeof val === "object") {
                    return JSON.stringify(val)
                }
                return toString(val)
            }
        }, opts.filters || {});
        var stack = [create(data || {})],
            output = [];
        return {
            get: get,
            set: set,
            push: push,
            pop: pop,
            write: write,
            filter: filter,
            each: each,
            block: block,
            render: render
        }
    };
    var runtime;
    jinja.compile = function(markup, opts) {
        opts = opts || {};
        var parser = new Parser;
        parser.readTemplateFile = this.readTemplateFile;
        var code = [];
        code.push("function render($) {");
        code.push("var get = $.get, set = $.set, push = $.push, pop = $.pop, write = $.write, filter = $.filter, each = $.each, block = $.block;");
        code.push.apply(code, parser.parse(markup));
        code.push("return $.render();");
        code.push("}");
        code = code.join("\n");
        if (opts.runtime === false) {
            var fn = new Function("data", "options", "return (" + code + ")(runtime(data, options))")
        } else {
            runtime = runtime || (runtime = getRuntime.toString());
            fn = new Function("data", "options", "return (" + code + ")((" + runtime + ")(data, options))")
        }
        return {
            render: fn
        }
    };
    jinja.render = function(markup, data, opts) {
        var tmpl = jinja.compile(markup);
        return tmpl.render(data, opts)
    };
    jinja.templateFiles = [];
    jinja.readTemplateFile = function(name) {
        var templateFiles = this.templateFiles || [];
        var templateFile = templateFiles[name];
        if (templateFile == null) {
            throw new Error("Template file not found: " + name)
        }
        return templateFile
    };

    function trimLeft(str) {
        return str.replace(LEADING_SPACE, "")
    }

    function trimRight(str) {
        return str.replace(TRAILING_SPACE, "")
    }

    function matchAll(str, reg, fn) {
        reg = new RegExp(reg.source, "g" + (reg.ignoreCase ? "i" : "") + (reg.multiline ? "m" : ""));
        var match;
        while (match = reg.exec(str)) {
            var result = fn(match[0], match.index, str);
            if (typeof result == "number") {
                reg.lastIndex = result
            }
        }
    }
});